import json
import logging
import traceback
import google.protobuf.json_format as json_format

from cc.appcoin2.support.order.py_parser import PyOrderParser
from coin.base.param_util import to_list
from coin.exchange.bybit.kr_rest.native_private_client import BybitNativePrivateClient
from coin.exchange.bybit.kr_rest.futures_product import BybitFuturesProduct, BybitCurrency
from coin.exchange.bybit.kr_rest.product_info import bybit_product_info
from coin.exchange.bybit.kr_rest.constants import balance_currency_list, inverse_symbol_list, usdt_symbol_list
from coin.exchange.kr_rest.product.product_impl import generate_product_from_str2
import coin.exchange.base.kr_rest.private_client_base as privcb
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       CurrencyBalance,
                                       AccountPosition,
                                       ProductPosition)
from coin2.exchange.order.system_pb2 import OrderSystemConfig
from coin2.exchange.symbology_pb2 import ProductConfig


class BybitFuturesPrivateParser(object):
  @staticmethod
  def parse_balance(update_msg):
    """
    {
      "ret_code": 0,
      "ret_msg": "OK",
      "ext_code": "",
      "ext_info": "",
      "result": {
          "BTC": {
              "equity": 1002,                         //equity = wallet_balance + unrealised_pnl
              "available_balance": 999.99987471,      //available_balance
              //In Isolated Margin Mode:
              // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin)
              //In Cross Margin Mode:
                //if unrealised_pnl > 0:
                //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin)；
                //if unrealised_pnl < 0:
                //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl
              "used_margin": 0.00012529,              //used_margin = wallet_balance - available_balance
              "order_margin": 0.00012529,             //Used margin by order
              "position_margin": 0,                   //position margin
              "occ_closing_fee": 0,                   //position closing fee
              "occ_funding_fee": 0,                   //funding fee
              "wallet_balance": 1000,                 //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance.
              "realised_pnl": 0,                      //daily realized profit and loss
              "unrealised_pnl": 2,                    //unrealised profit and loss
                  //when side is sell:
                  // unrealised_pnl = size * (1.0 / mark_price -  1.0 / entry_price）
                  //when side is buy:
                  // unrealised_pnl = size * (1.0 / entry_price -  1.0 / mark_price）
              "cum_realised_pnl": 0,                  //total relised profit and loss
              "given_cash": 0,                        //given_cash
              "service_cash": 0                       //service_cash
          }
      },
      "time_now": "1578284274.816029",
      "rate_limit_status": 98,
      "rate_limit_reset_ms": 1580885703683,
      "rate_limit": 100
    }
    """
    assert update_msg['ret_msg'] == 'OK' and 'result' in update_msg, update_msg
    currency_balance_list = []
    balance_info = update_msg['result']
    assert len(balance_info) == 1
    for native_currency, balance_detail in balance_info.items():
      currency = BybitCurrency.FromStrNativeCurrency(native_currency)
      unrealized_pnl = float(balance_detail['unrealised_pnl'])
      total = float(balance_detail['equity'])
      hold = float(balance_detail['used_margin'])
      available = float(balance_detail['available_balance'])
      currency_balance = CurrencyBalance(currency=currency.currency,
                                         currency_native=currency.native_currency,
                                         total=total,
                                         hold=hold,
                                         available=available)
      currency_balance_list.append(currency_balance)

      return AccountBalance(
          exchange='Bybit',
          each_balance=currency_balance_list,
      )

  @staticmethod
  def parse_balance_total(update_msg_total):
    currency_balance_list = []
    for update_msg in update_msg_total:
      account_balance = AccountBalance()
      json_format.Parse(update_msg, account_balance)
      try:
        assert account_balance.HasField('market_type')
        assert account_balance.HasField('exchange')
      except Exception as e:
        logging.error(traceback.format_exc())
        continue
      for balance in account_balance.each_balance:
        currency_balance_list.append(balance)
    return AccountBalance(
        market_type='Futures',
        exchange='Bybit',
        each_balance=currency_balance_list,
    )

  @staticmethod
  def parse_inverse_position(pos_infos):
    """
    [{
      "result": {
        "id": 27913,
        "user_id": 1,
        "risk_id": 1,
        "symbol": "BTCUSD",
        "side": "Buy",
        "size": 5,
        "position_value": "0.0006947",
        "entry_price": "7197.35137469",
        "auto_add_margin": 0,
        "leverage": "1",  //逐仓模式下, 值为用户设置的杠杆；全仓模式下，值为当前风险限额下最大杠杆
        "effective_leverage": "1", // 有效杠杆. 逐仓模式下, 等于`leverage`; 全仓模式下, 计算公式为： position size / markprice / (wallet_balance+unrealised_pnl)
        "position_margin": "0.0006947",
        "liq_price": "3608",
        "bust_price": "3599",
        "occ_closing_fee": "0.00000105",
        "occ_funding_fee": "0",
        "take_profit": "0",
        "stop_loss": "0",
        "trailing_stop": "0",
        "position_status": "Normal",
        "deleverage_indicator": 4,
        "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}",
        "order_margin": "0.00029477",
        "wallet_balance": "0.03000227",
        "realised_pnl": "-0.00000126",
        "unrealised_pnl": 0,
        "cum_realised_pnl": "-0.00001306",
        "cross_seq": 444081383,
        "position_seq": 287141589,
        "created_at": "2019-10-19T17:04:55Z",
        "updated_at": "2019-12-27T20:25:45.158767Z"
      },
    }]
    """
    assert isinstance(pos_infos, list)
    each_position = []
    for pos_info in pos_infos:
      native_symbol = pos_info['symbol']
      product = BybitFuturesProduct.FromStrNativeProduct(native_symbol)
      position = ProductPosition(symbol=product.symbol)
      if pos_info['side'] == 'Buy':
        position.long_position = float(pos_info['size'])
      elif pos_info['side'] == 'Sell':
        position.short_position = float(pos_info['size'])
      else:
        position.short_position = 0.
        position.long_position = 0.
      assert position.long_position >= 0 and position.short_position >= 0, position
      position.net_position = position.long_position - position.short_position
      each_position.append(position)
    return AccountPosition(exchange='Bybit', each_position=each_position)

  @staticmethod
  def parse_inverse_perpetual_position(update_msg):
    """
    {
    "ret_code": 0,
    "ret_msg": "OK",
    "ext_code": "",
    "ext_info": "",
    "result": {
        ...
      },
      "time_now": "1577480599.097287",
      "rate_limit_status": 119,
      "rate_limit_reset_ms": 1580885703683,
      "rate_limit": 120
    }
    """
    assert update_msg['ret_msg'] == 'OK' and 'result' in update_msg, update_msg
    pos_infos = to_list(update_msg['result'])
    return BybitFuturesPrivateParser.parse_inverse_position(pos_infos)


  @staticmethod
  def parse_inverse_delivery_position(update_msg):
    """
    {
    "ret_code": 0,
    "ret_msg": "OK",
    "ext_code": "",
    "ext_info": "",
    "result": {
        ...
      },
      "time_now": "1577480599.097287",
      "rate_limit_status": 119,
      "rate_limit_reset_ms": 1580885703683,
      "rate_limit": 120
    }
    """
    assert update_msg['ret_msg'] == 'OK' and 'result' in update_msg, update_msg
    pos_infos = []
    for pos_info in update_msg['result']:
      if pos_info['is_valid'] is True:
        pos_infos.append(pos_info['data'])
    return BybitFuturesPrivateParser.parse_inverse_position(pos_infos)

  @staticmethod
  def parse_usdt_position(update_msg):
    assert update_msg['ret_msg'] == 'OK' and 'result' in update_msg, update_msg
    pos_info = update_msg['result']
    assert len(pos_info) == 2
    position = None
    for position_detail in pos_info:
      if position is None:
        native_symbol = position_detail['symbol']
        product = BybitFuturesProduct.FromStrNativeProduct(native_symbol)
        position = ProductPosition(symbol=product.symbol)
      if position_detail['side'] == 'Buy':
        position.long_position = float(position_detail['size'])
      elif position_detail['side'] == 'Sell':
        position.short_position = float(position_detail['size'])
      else:
        raise ValueError('Invalid side!')
    assert position.long_position >= 0 and position.short_position >= 0, position
    position.net_position = position.long_position - position.short_position
    return AccountPosition(exchange='Bybit', each_position=[position])
    return

  @staticmethod
  def parse_position_total(update_msg_total):
    position_list = []
    for update_msg in update_msg_total:
      account_position = AccountPosition()
      json_format.Parse(update_msg, account_position)
      #assert account_position.HasField('market_type')
      #assert account_position.HasField('exchange')
      for pos in account_position.each_position:
        position_list.append(pos)
    return AccountPosition(market_type='Futures', exchange='Bybit', each_position=position_list)


class BybitPrivateClient(privcb.PrivateClientBase):
  def __init__(self, key_file, timeout=20):
    privcb.PrivateClientBase.__init__(self)
    self.nprivc = BybitNativePrivateClient(key_file=key_file, timeout=timeout)
    pi_bundle = bybit_product_info._product_infos
    assert pi_bundle.HasField('mea')
    # TODO(chensili): remove pi.native_symbol.endswith('USD')
    # after parsing of delivery products supported.
    norms = [pi.symbol for pi in pi_bundle.product_infos if
             pi.native_symbol in inverse_symbol_list and
             pi.native_symbol.endswith('USD')]
    oss_config = OrderSystemConfig(
        mea=pi_bundle.mea,
        products=ProductConfig(norms=norms))
    self._parser = PyOrderParser.from_mea_str('Futures.Bybit.v2')
    self._parser.add_product(oss_config.SerializeToString())

  def query_account_balance_impl(self):
    update_msg = []
    for currency in balance_currency_list:
      update = self.nprivc.query_account_balance(coin=currency)
      msg = self._parser.parse_account_balance(json.dumps(update.msg))
      update_msg.append(msg)
    update.msg = BybitFuturesPrivateParser.parse_balance_total(update_msg)
    return update

  def query_account_position_impl(self):
    update_msg = []
    for symbol in inverse_symbol_list:
      # TODO(chensili): remove this after parsing of delivery products supported.
      if not symbol.endswith('USD'):
        continue
      product = generate_product_from_str2('Futures', 'Bybit', 'v2', symbol)
      if product.contract_type.lower() == 'perpetual':
        update = self.nprivc.query_inverse_perpetual_position(symbol=symbol)
      else:
        update = self.nprivc.query_inverse_delivery_position(symbol=symbol)
      update.msg = self._parser.parse_account_position(json.dumps(update.msg))
      update_msg.append(update.msg)
    update.msg = BybitFuturesPrivateParser.parse_position_total(update_msg)
    return update

  def query_list_orders_impl(self, product):
    return NotImplementedError

  def query_cancel_product_impl(self, product):
    raise NotImplementedError

  def query_fills_impl(self, product):
    raise NotImplementedError

  def query_submit_impl(self, product, side, price, qty):
    raise NotImplementedError


class BybitLinearPrivateClient(privcb.PrivateClientBase):
  def __init__(self, key_file, timeout=20):
    privcb.PrivateClientBase.__init__(self)
    self.nprivc = BybitNativePrivateClient(key_file=key_file, timeout=timeout)
    pi_bundle = bybit_product_info._product_infos
    assert pi_bundle.HasField('mea')
    norms = [pi.symbol for pi in pi_bundle.product_infos if
             pi.native_symbol in usdt_symbol_list]
    oss_config = OrderSystemConfig(
        mea=pi_bundle.mea,
        products=ProductConfig(norms=norms))
    self._parser = PyOrderParser.from_mea_str('Futures.Bybit.v2-linear')
    self._parser.add_product(oss_config.SerializeToString())

  def query_account_balance_impl(self):
    update_msg = []
    for currency in balance_currency_list:
      update = self.nprivc.query_account_balance(coin=currency)
      msg = self._parser.parse_account_balance(json.dumps(update.msg))
      update_msg.append(msg)
    update.msg = BybitFuturesPrivateParser.parse_balance_total(update_msg)
    return update

  def query_account_position_impl(self):
    update_msg = []
    for symbol in usdt_symbol_list:
      update = self.nprivc.query_usdt_position(symbol=symbol)
      update.msg = self._parser.parse_account_position(json.dumps(update.msg))
      update_msg.append(update.msg)
    update.msg = BybitFuturesPrivateParser.parse_position_total(update_msg)
    return update

  def query_list_orders_impl(self, product):
    return NotImplementedError

  def query_cancel_product_impl(self, product):
    raise NotImplementedError

  def query_fills_impl(self, product):
    raise NotImplementedError

  def query_submit_impl(self, product, side, price, qty):
    raise NotImplementedError


if __name__ == '__main__':
  client = BybitLinearPrivateClient(key_file='../../coin_key/view/xunke02_linear/bybit/view_key.json')
  #client = BybitPrivateClient(key_file='../../coin_key/view/xunke05/bybit/view_key.json')
  print(client.query_account_balance())
  print(client.query_account_position())
